// (c) 2015 Teresa Holfeld
package com.holfeld.teresa.vocabtrainer.provider;

import android.content.Context;
import android.util.SparseArray;

import com.holfeld.teresa.vocabtrainer.R;
import com.holfeld.teresa.vocabtrainer.util.FileIO;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * The VocabularyPool is the container for all vocabularies (flash card stacks). It manages the
 * training set, the set of flash cards that the user is currently training. It also manages all
 * manipulations for the vocabularies and vocable pairs, such as adding, renaming, editing locales,
 * or removing of vocabularies of vocables.
 *
 * @author Teresa Holfeld, teresa.holfeld@gmail.com
 */
public class VocabularyPool {

    /** The name of the default example vocabulary. */
    private String mExampleVocabulary;

    /** Maps vocabularies (values) by their index (keys). */
    private SparseArray<Vocabulary> mVocabularyMap;

    /** The index of the active vocabulary in the vocabulary map. */
    private int mActiveVocabulary;

    /**
     * The stack of vocable pairs that are currently trained. These are the vocable pairs from the
     * active vocabulary minus the ones marked as known.
     */
    private List<VocablePair> mTrainingSet;

    /** The Android context. */
    private Context mContext;

    /** The singleton instance. */
    private static VocabularyPool mInstance;

    /**
     * Singleton: constructor is private.
     *
     * @param context the Android context
     */
    private VocabularyPool(Context context) {
        mContext = context;
        mVocabularyMap = new SparseArray<Vocabulary>();
        mExampleVocabulary = context.getResources().getString(R.string.example_vocabulary);
        loadVocabularies();
    }

    /**
     * Gets the VocabularyPool singleton instance.
     *
     * @param context the Android context
     * @return the singleton instance
     */
    public static VocabularyPool getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new VocabularyPool(context);
        }
        return mInstance;
    }

    /**
     * @return the first vocable from the shuffled training set
     */
    public VocablePair getRandomVocable() {
        if (mTrainingSet.size() > 0) {
            return mTrainingSet.get(0);
        } else {
            return null;
        }
    }

    /**
     * If the given vocable has been marked as right by the user, remove the vocable from the
     * training set.
     *
     * @param vocable the vocable that was right
     * @return true if the training set still has flash cards, false if it is empty
     */
    public boolean wasRight(VocablePair vocable) {
        // TODO: use vocable for statistics
        mTrainingSet.remove(0);
        return (mTrainingSet.size() != 0);
    }

    /**
     * If the given vocable has been marked as wrong by the user, just shuffle the training set
     * again.
     *
     * @param vocable the vocable that was wrong
     */
    public void wasWrong(VocablePair vocable) {
        // TODO: use vocable for statistics
        Collections.shuffle(mTrainingSet);
        // repeat until first element is different
        if (mTrainingSet.size() > 1 && mTrainingSet.get(0).equals(vocable)) {
            wasWrong(vocable);
        }
    }

    /**
     * Clears the training set, fills it with the vocable pairs from the active vocabulary, and
     * shuffles them.
     */
    public void refillTrainingSet() {
        mTrainingSet.clear();
        mTrainingSet.addAll(mVocabularyMap.get(mActiveVocabulary).getVocablePairs());
        Collections.shuffle(mTrainingSet);
    }

    /**
     * Adds a new vocable to the active vocabulary.
     *
     * @param vocable1 first value of the new vocable pair
     * @param vocable2 second value of the new vocable pair
     */
    public synchronized void addVocableToActiveVocabulary(String vocable1, String vocable2) {
        Vocabulary vocabulary = mVocabularyMap.get(mActiveVocabulary);
        VocablePairFactory vocableFactory =
                new VocablePairFactory(vocabulary.getLocale1(), vocabulary.getLocale2());
        VocablePair vocablePair = vocableFactory.createVocablePair(vocable1, vocable2);
        vocabulary.put(vocablePair);
        mVocabularyMap.put(mActiveVocabulary, vocabulary);
        FileIO.saveToStorage(mContext, vocabulary);
        refillTrainingSet();
    }

    /**
     * Adds a new vocable to the given vocabulary.
     *
     * @param vocabularyName the name of the vocabulary the new vocable will be added to
     * @param vocable1 first value of the new vocable pair
     * @param vocable2 second value of the new vocable pair
     */
    public synchronized void addVocable(String vocabularyName, String vocable1, String vocable2) {
        try {
            VocabularyEntry vocabularyEntry = getVocabularyEntry(vocabularyName);
            Vocabulary vocabulary = vocabularyEntry.vocabulary;
            Integer index = vocabularyEntry.index;
            VocablePairFactory vocableFactory =
                    new VocablePairFactory(vocabulary.getLocale1(), vocabulary.getLocale2());
            VocablePair vocablePair = vocableFactory.createVocablePair(vocable1, vocable2);
            vocabulary.put(vocablePair);
            mVocabularyMap.put(index, vocabulary);
            FileIO.saveToStorage(mContext, vocabulary);
        } catch (NullPointerException e) {
            e.printStackTrace();
        }
    }

    /**
     * Edits the given old vocable pair to the given new vocable pair.
     *
     * @param vocabularyName the name of the vocabulary where to edit the vocable
     * @param oldVocable1 first value of the old vocable pair
     * @param oldVocable2 second value of the old vocable pair
     * @param newVocable1 first value of the new vocable pair
     * @param newVocable2 second value of the new vocable pair
     */
    public synchronized void editVocable(String vocabularyName, String oldVocable1,
                                         String oldVocable2, String newVocable1,
                                         String newVocable2) {
        try {
            VocabularyEntry vocabularyEntry = getVocabularyEntry(vocabularyName);
            Vocabulary vocabulary = vocabularyEntry.vocabulary;
            Integer index = vocabularyEntry.index;
            VocablePairFactory vocableFactory =
                    new VocablePairFactory(vocabulary.getLocale1(), vocabulary.getLocale2());
            VocablePair oldVocablePair = vocableFactory.createVocablePair(oldVocable1, oldVocable2);
            VocablePair newVocablePair = vocableFactory.createVocablePair(newVocable1, newVocable2);
            vocabulary.delete(oldVocablePair);
            vocabulary.put(newVocablePair);
            mVocabularyMap.put(index, vocabulary);
            FileIO.saveToStorage(mContext, vocabulary);
        } catch (NullPointerException e) {
            e.printStackTrace();
        }
    }

    /**
     * Deletes the given vocable pair from the given vocabulary.
     *
     * @param vocabularyName name of the vocabulary
     * @param vocable vocable pair to delete
     */
    public void deleteVocable(String vocabularyName, VocablePair vocable) {
        try {
            VocabularyEntry vocabularyEntry = getVocabularyEntry(vocabularyName);
            Vocabulary vocabulary = vocabularyEntry.vocabulary;
            Integer index = vocabularyEntry.index;
            vocabulary.delete(vocable);
            mVocabularyMap.put(index, vocabulary);
            FileIO.saveToStorage(mContext, vocabulary);
        } catch (NullPointerException e) {
            e.printStackTrace();
        }
    }

    /**
     * Deletes the vocable pair of the two given vocables from the given vocabulary.
     *
     * @param vocabularyName name of the vocabulary
     * @param vocable1 the first vocable of the VocablePair to delete
     * @param vocable2 the second vocable of the VocablePair to delete
     */
    public void deleteVocable(String vocabularyName, String vocable1, String vocable2) {
        Vocabulary vocabulary = getVocabulary(vocabularyName);
        if (vocabulary == null) {
            return;
        }
        vocabulary.delete(vocable1, vocable2);
    }

    /**
     * Adds a new, empty vocabulary with the given name and locales to the vocabulary map.
     *
     * @param name name of the new vocabulary
     * @param locale1 locale 1 of the new vocabulary
     * @param locale2 locale 2 of the new vocabulary
     */
    public synchronized void addVocabulary(String name, String locale1, String locale2) {
        Vocabulary vocabulary = new Vocabulary(name, locale1, locale2);
        mVocabularyMap.put(mVocabularyMap.size(), vocabulary);
        FileIO.saveToStorage(mContext, vocabulary);
    }

    /**
     * Edits the vocabulary info. The title of the vocabulary will be renamed to the given newName,
     * and the locales will be changed to the given newLocale1 and newLocale2.
     *
     * @param oldName the initial vocabulary name
     * @param newName the new vocabulary name to set
     * @param newLocale1 the new locale 1 to set
     * @param newLocale2 the new locale 2 to set
     */
    public synchronized void editVocabularyInfo(String oldName, String newName, String newLocale1,
                                                String newLocale2) {
        Vocabulary vocabulary = getVocabulary(oldName);
        List<VocablePair> vocables = new ArrayList<VocablePair>();
        vocables.addAll(vocabulary.getVocablePairs());
        deleteVocabulary(oldName);
        List<VocablePair> newVocables = editVocableLocales(vocables, newLocale1, newLocale2);
        Vocabulary newVocabulary = new Vocabulary(newName, newLocale1, newLocale2);
        for (VocablePair vocable : newVocables) {
            newVocabulary.put(vocable);
        }
        mVocabularyMap.put(mVocabularyMap.size(), newVocabulary);
        FileIO.saveToStorage(mContext, newVocabulary);
    }

    /**
     * Sets the active vocabulary to the one specified by given name.
     *
     * @param vocabularyName name of the vocabulary to set active
     */
    public void setActiveVocabulary(String vocabularyName) {
        mActiveVocabulary = 0;
        try {
            for (int i = 0; i < mVocabularyMap.size(); i++) {
                if (mVocabularyMap.valueAt(i).getName().equals(vocabularyName)) {
                    mActiveVocabulary = mVocabularyMap.keyAt(i);
                }
            }
        } catch (NullPointerException e) {
            mActiveVocabulary = 0;
            e.printStackTrace();
        }
        refillTrainingSet();
    }

    /**
     * @return name of active vocabulary
     */
    public String getNameOfActiveVocabulary() {
        return mVocabularyMap.get(mActiveVocabulary).getName();
    }

    /**
     * @return list of names of all vocabularies in the vocabulary map
     */
    public List<String> getNames() {
        List<String> names = new ArrayList<String>();
        for (int i = 0; i < mVocabularyMap.size(); i++) {
            names.add(mVocabularyMap.valueAt(i).getName());
        }
        return names;
    }

    /**
     * Gets the vocabulary with the given name.
     *
     * @param vocabularyName name of the vocabulary
     * @return the vocabulary, or null if it doesn't exist in the vocabulary map
     */
    public Vocabulary getVocabulary(String vocabularyName) {
        for (int i = 0; i < mVocabularyMap.size(); i++) {
            Vocabulary vocabulary = mVocabularyMap.valueAt(i);
            if (vocabulary.getName().equals(vocabularyName)) {
                return vocabulary;
            }
        }
        return null;
    }

    /**
     * @return list of all vocabularies in the vocabulary map
     */
    public List<Vocabulary> getVocabularies() {
        List<Vocabulary> vocabularies = new ArrayList<Vocabulary>();
        for (int i = 0; i < mVocabularyMap.size(); i++) {
            vocabularies.add(mVocabularyMap.valueAt(i));
        }
        return vocabularies;
    }

    /**
     * Deletes the given vocabulary.
     *
     * @param vocabularyName name of the vocabulary to delete
     */
    public void deleteVocabulary(String vocabularyName) {
        FileIO.deleteVocabulary(mContext, vocabularyName);
        loadVocabularies();
    }

    /**
     * Gets locale 1 of the given vocabulary.
     *
     * @param vocabularyName name of the vocabulary
     * @return the vocabulary's first locale
     */
    public String getLocale1(String vocabularyName) {
        for (int i = 0; i < mVocabularyMap.size(); i++) {
            Vocabulary vocabulary = mVocabularyMap.valueAt(i);
            if (vocabulary.getName().equals(vocabularyName)) {
                return vocabulary.getLocale1();
            }
        }
        return null;
    }

    /**
     * Gets locale 2 of the given vocabulary.
     *
     * @param vocabularyName name of the vocabulary
     * @return the vocabulary's second locale
     */
    public String getLocale2(String vocabularyName) {
        for (int i = 0; i < mVocabularyMap.size(); i++) {
            Vocabulary vocabulary = mVocabularyMap.valueAt(i);
            if (vocabulary.getName().equals(vocabularyName)) {
                return vocabulary.getLocale2();
            }
        }
        return null;
    }

    /**
     * @return locale 1 of the active vocabulary
     */
    public String getLocale1OfActiveVocabulary() {
        return mVocabularyMap.get(mActiveVocabulary).getLocale1();
    }

    /**
     * @return locale 2 of the active vocabulary
     */
    public String getLocale2OfActiveVocabulary() {
        return mVocabularyMap.get(mActiveVocabulary).getLocale2();
    }

    /**
     * @return the size of the active vocabulary
     */
    public int getTotal() {
        return mVocabularyMap.get(mActiveVocabulary).size();
    }

    /**
     * @return the progress (total - training set size)
     */
    public int getProgress() {
        return getTotal() - mTrainingSet.size();
    }


    /**
     * For the given vocable pairs, changes the locales to given newLocale1 and newLocale2.
     *
     * @param vocables the vocable pairs where the locales should be changed
     * @param newLocale1 the new locale 1
     * @param newLocale2 the new locale 2
     * @return list of vocable pairs with new locales
     */
    private List<VocablePair> editVocableLocales(List<VocablePair> vocables, String newLocale1,
                                                 String newLocale2) {
        List<VocablePair> newVocables = new ArrayList<VocablePair>();
        VocablePairFactory factory = new VocablePairFactory(newLocale1, newLocale2);
        for (VocablePair vocable : vocables) {
            VocablePair newVocable = factory.createVocablePair(vocable.getVocable1(),
                    vocable.getVocable2());
            newVocables.add(newVocable);
        }
        return newVocables;
    }

    /**
     * Loads all vocabularies from the app's internal storage and puts it into the vocabulary map.
     */
    private void loadVocabularies() {
        mVocabularyMap.clear();
        if (!FileIO.builtInVocabulariesExist(mContext)) {
            List<Vocabulary> builtInVocabularies = FileIO.getBuiltInVocabularies(mContext);
            for (Vocabulary vocabulary : builtInVocabularies) {
                FileIO.saveToStorage(mContext, vocabulary);
            }
        }
        Vocabulary exampleVocabulary = FileIO.readFromStorage(mContext, mExampleVocabulary);
        mVocabularyMap.put(0, exampleVocabulary);
        mActiveVocabulary = 0;
        List<Vocabulary> vocabularies = FileIO.getVocabularies(mContext);
        for (Vocabulary vocabulary : vocabularies) {
            if (vocabulary.getName().equals(mExampleVocabulary)) continue;
            mVocabularyMap.put(mVocabularyMap.size(), vocabulary);
        }
        mTrainingSet = new ArrayList<VocablePair>();
        refillTrainingSet();
    }

    /**
     * Looks up the given vocabulary name in the vocabulary map and returns the vocabulary and its
     * index in the map as VocabularyEntry.
     *
     * @param vocabularyName the name of the vocabulary to look up
     * @return the vocabulary and index
     * @throws NullPointerException if the given vocabulary does not exist in the vocabulary map
     */
    private VocabularyEntry getVocabularyEntry(String vocabularyName) throws NullPointerException {
        VocabularyEntry entry = new VocabularyEntry();
        for (int i = 0; i < mVocabularyMap.size(); i++) {
            Vocabulary v = mVocabularyMap.valueAt(i);
            if (v.getName().equals(vocabularyName)) {
                entry.vocabulary = v;
                entry.index = mVocabularyMap.keyAt(i);
            }
        }
        if (entry.vocabulary == null || entry.index == null) {
            throw new NullPointerException("Vocabulary '" + vocabularyName + "' does not exist.");
        }
        return entry;
    }

    /**
     * Vocabulary map entry with Vocabulary and its index in the map.
     */
    private class VocabularyEntry {
        public Vocabulary vocabulary;
        public Integer index;
    }
}
